package util

import (
	"fmt"
	"hash/crc32"
	"log"
	"math/rand"
	"sort"
	"time"
)

/**
负载均衡简易健康检查机制
1.定期向url发送http请求（结合注册中心则可以直接用注册中心的可用服务列表），并根据接口响应码和响应时间判断
	发送什么请求：
		一般做法是发送Head请求（GET/POST|PUT)
		只返回头部，不需要返回body部分，使得传输量较小，可以用来检测Url是否可用
2.如果异常，在阈值达到后标记为不可用
3.异常节点恢复后，恢复节点的可用状态
思路：给每个server结构体添加一个状态节点，添加一个goRouting去定时访问所有的服务，然后根据结果修改状态节点。

如果所有服务都down了，怎么处理：
视频给的答案：
1.最后一个服务始终不设置为down
2.获取Server时不再判断是否down，依然照样轮询所有 --这个个人感觉是可以的，
3.proxy直接返回一个 友好界面 404 502 啊哦，网页飞走了~
*/
//平滑加权轮询算法下的降权机制所需
type HttpServerSlice []*HttpServer

// 根据负载均衡器创建时设定的路由方式选择合适的路由，在这里进行简要配置
var POLLING = "polling"
var SWPOLLING = "sw-polling"
var IPHASH = "ip-hash"

// 让该切片支持排序
func (x HttpServerSlice) Len() int           { return len(x) }
func (x HttpServerSlice) Less(i, j int) bool { return x[i].CWeight > x[j].CWeight } //从大到小排序
func (x HttpServerSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
func (x HttpServerSlice) CWeightToString() {
	fmt.Println("CWeight:")
	for _, v := range x {
		fmt.Print(" ", v.CWeight, " ")
	}
}
func (x HttpServerSlice) StatusToString() {
	fmt.Println("Status:")
	for _, v := range x {
		fmt.Println(" ", v.Host, " ", v.Status, " ", v.FailCount, " ", v.SuccessCount, " ", v.FailWeight)
	}
}

// 目标server类
type HttpServer struct {
	ServerName   string //服务名称
	Path         string //用于想单独访问该服务器时使用
	Host         string //http://localhost:9092
	Weight       int    //权重
	CWeight      int    //当前权重--平滑加权轮询算法所需
	Status       string //当前状态，初始为UP down机则为DOWN 用于健康检查
	FailCount    int    //计数器机制，用于实现FailOver机制 失败超过一定次数才标状态为down 默认是0
	SuccessCount int    //成功计数器 虽然我感觉有点多余，但是没准是有他的道理的，成功超过一定次数才为up
	FailWeight   int    //FailOver机制下的降权机制， 用于加权轮询算法中。 降权机制：降权值FailWeight 每次失败，FailWeight+=weight*降权因子（写在Check中里） 加权轮询的过程中，真正的权重=weight-FailWeight，如果是0则不选择
}

var ServerIndices []int //加权随机算法所需
var weightCount int     //权重总和 改良加权随机算法所需  现已整合到LoadBalance中，自己整合，与视频无关。
// Init函数在该包被引入时会自动执行，基本概念
func init() {

}

// 随机打乱切片封装 变量前加&表示地址 指针前加*表示值传递 切片默认引用传递（传地址）
func randServerIndices(serverIndices []int) {
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(serverIndices), func(i, j int) {
		serverIndices[i], serverIndices[j] = serverIndices[j], serverIndices[i]
	})
}

// 初始化HttpServer
func NewHttpServer(serverName string, host string, weight int, path string) *HttpServer {
	return &HttpServer{ServerName: serverName, Host: host, Weight: weight, CWeight: 0, Status: "UP", Path: path}
}

// LoadBalance 负载均衡类
type LoadBalance struct {
	//Servers []*HttpServer
	Servers HttpServerSlice //由于平滑加权轮询算法定义了一个类型切片，因此这里也做相应修改，两者等价
	//轮询负载均衡需求添加
	CurIndex       int    //指向当前访问的服务器index 默认为0
	WeightCount    int    //为了确保可以同时有多个LoadBalance存在时可以先计算出权重总和而添加
	DownServers    int    //自己添加的该负载均衡当前down机数量,初始为0。
	LoadBalanceWay string // 负载均衡方式 // 值：polling:轮询 sw-polling:平滑加权轮询 ip-hash:ip-hash方式，可确保同一ip每次均访问同一服务器，避免session不共享等问题
}

// NewLoadBalance 自动初始化一个负载均衡类 需要传递参数loadBalanceWay 表示负载均衡方式：
// polling:轮询 sw-polling:平滑加权轮询 ip-hash:ip-hash方式，可确保同一ip每次均访问同一服务器，避免session不共享等问题
func NewLoadBalance(servers HttpServerSlice, loadBalanceWay string) *LoadBalance {
	//实例化一个LoadBalance对象
	return &LoadBalance{Servers: servers, CurIndex: 0, DownServers: 0, LoadBalanceWay: loadBalanceWay}
}

// StartCheck 初始化一个自定义httpCheck
//
//	 FailMax int //失败阈值 连续失败到达该阈值标识为down 默认是5  用于计数器机制的负载均衡-普通轮询
//		SuccessCount int //成功阈值 连续成功到达该值标识为up 默认是2 用于计数器机制的负载均衡-普通轮询
//		FailFactor float64 //降权因子，默认是5.0 增加降权值，增加量为该服务权重的1/5  用于降权机制的负载均衡-平滑加权轮询
//		RecoverFactor float64 //加权因子，默认是2.0 当健康检查成功时则减少降权值 减少量为当前服务权重的1/2  用于降权机制的负载均衡-平滑加权轮询
//		checkTime int //检查时间，设置该负载均衡多久对服务执行一次健康检查 默认为5s
func (this *LoadBalance) StartCheck(failMax int, successCount int, failFactor float64, recoverFactor float64, checkTime int) {
	go (func() {
		//创建一个定时器，根据设定时间执行
		t := time.NewTicker(time.Second * time.Duration(checkTime)) //转换成Duration变量再相乘
		check := NewHttpChecker(this, failMax, successCount, failFactor, recoverFactor)
		for {
			select {
			case <-t.C:
				//过期时间 这个后面再补
				check.Check(time.Second * 2)
				this.Servers.StatusToString()
				fmt.Println(this.DownServers)
				fmt.Println("------------------------------------------------")
			}
		}
	})()
}

// StartNormalCheck 初始化一个httpCheck
func (this *LoadBalance) StartNormalCheck() {
	go (func() {
		//创建一个定时器，每三秒执行一次  实际是每隔三秒往Channel里塞一个值 通过t.C判断 后修改成15
		t := time.NewTicker(time.Second * 15)
		check := NewSingleHttpChecker(this)
		for {
			select {
			case <-t.C:
				//过期时间 这个后面再补
				check.Check(time.Second * 2)
				/*				this.Servers.StatusToString()
								fmt.Println(this.DownServers)
								fmt.Println("------------------------------------------------")*/
			}
		}
	})()
}

// 应该是谁调用这个函数，这个this就指代谁吧 是的 代表对象
func (this *LoadBalance) AddServer(server *HttpServer) {
	//因为servers是一个切片
	this.Servers = append(this.Servers, server)
	this.WeightCount += server.Weight
}

// 随机算法
func (this *LoadBalance) SelectForRand() *HttpServer {
	//取随机数
	rand.Seed(time.Now().UnixNano())
	//取到0-n之间
	index := rand.Intn(len(this.Servers))
	return this.Servers[index]
}

// SelectByWeightRand 加权随机算法
func (this *LoadBalance) SelectByWeightRand() *HttpServer {
	//取随机数
	rand.Seed(time.Now().UnixNano())
	//取到0-n之间
	index := rand.Intn(len(ServerIndices))
	//从ServerIndices中取出加权后的随机值，并通过该随机值取出最后Server
	return this.Servers[ServerIndices[index]]
}

// SelectByWeightRandNew
// 改良加权随机算法 根据比重计算 思路很简单
// 将用户输入的比重相加
// 然后确定每个比重所在区间
// 选择随机数，随机数落在哪个区间就选择哪个区间对应的服务
func (this *LoadBalance) SelectByWeightRandNew() *HttpServer {
	//取随机数
	rand.Seed(time.Now().UnixNano())
	//取随机数 这一步不能这样写，除非只有一个LB，当多个LB时这里这样写死会出Bug
	index := rand.Intn(weightCount) //左闭右开[0,8)->0->7
	//所以要达到这个效果就只能提前算好并缓存起来。
	//这里存比重，我猜是最后随机数与这里的比重比较，从小开始比较，小于哪个就返回哪个的key值对应的服务
	/*sumList:=make([]int,len(this.Servers))*/
	/*//计算总和
	sum:=0
	for i:=0;i<len(this.Servers);i++{
		sum+=this.Servers[i].Weight
		sumList[i]=sum
	}*/

	//可以在这一步直接得出结果，这里进行尝试 如果要灵活的话，比如说有多个负载均衡配置，还是只能在每次使用时都计算一次权重，除非每次计算后缓存起来，感觉直接丢redis也是个不错的选择
	//经过尝试，是不行的，因为随机数的选取必须要有权重值的和，就必须经过一次相加，如果不是提前算好的话就必须相加一次

	weight := 0
	for i := 0; i < len(this.Servers); i++ {
		weight += this.Servers[i].Weight
		if index < weight {
			return this.Servers[i] //index不可能小于0，因此不用再做判断
		}
	}
	return this.Servers[0]
}

// SelectByIPHash ip_hash算法
func (this *LoadBalance) SelectByIPHash(ip string) *HttpServer {
	index := int(crc32.ChecksumIEEE([]byte(ip))) % len(this.Servers)
	return this.Servers[index]
}

// RoundRobin 轮询算法 使用了计数器机制
func (this *LoadBalance) RoundRobin() *HttpServer {
	//取出当前的服务器
	server := this.Servers[this.CurIndex]
	//一般来说，这些步骤为了线程安全都是要加锁的，否则会出现多次加一或多次置零的问题。 不需要说太多，只回答关键信息就行，不想回就暂时不回
	/*this.CurIndex++
	if this.CurIndex>=len(this.Servers){
		this.CurIndex=0
	}*/
	//改良步骤，可以确保不会超过最大值 因余数一定不大于取余的数，
	//当前一个数不大于后一个数时，取余得到的结果为他本身
	//相同数取余=0,这样就达到了大于等于最大值时自己置为0的效果
	this.CurIndex = (this.CurIndex + 1) % len(this.Servers)
	//添加健康机制后进行的补充，判断获取到的server是否为down，不为down才返回，否则再次获取新服务
	//加一条判断条件，当全部都为down时
	if server.Status == "DOWN" && !this.IsAllDown() {
		return this.RoundRobin() //递归 需注意一个问题，当所有机器都被down掉时这里会无限递归最后会超过栈深度报错。
	}

	return server
}

// 加权轮询算法
// 最简单的思路，结合加权随机算法和轮询算法，
// 将权重放到数组里，随机打乱然后轮询输出权重的值，感觉应该也会有改良版，   实际开发不建议使用
// 加权随机算法的改良版好像在这不太适用  使用加权随机算法的区间来做
func (this *LoadBalance) RoundRobinByWeight() *HttpServer {
	server := this.Servers[ServerIndices[this.CurIndex]]
	this.CurIndex = (this.CurIndex + 1) % len(ServerIndices) //注意细节
	return server
}

// 加权轮询算法的改良版 使用区间判断 补充降权健康机制
func (this *LoadBalance) RoundRobinByWeight2() *HttpServer {
	server := this.Servers[0]
	sum := 0
	for i := 0; i < len(this.Servers); i++ {
		realWeight := this.Servers[i].Weight - this.Servers[i].FailWeight
		if realWeight == 0 {
			continue //如果该值为0，说明该服务不可用，跳过
		}
		sum += this.Servers[i].Weight
		if this.CurIndex < sum {
			server = this.Servers[i]
			if this.CurIndex == sum-1 && i != len(this.Servers)-1 {
				this.CurIndex++
			} else {
				this.CurIndex = (this.CurIndex + 1) % sum
			}
			break
		} else {
			this.CurIndex = 0
		}
	}
	return server
}

// RoundRobinByWeight3
// 平滑加权轮询算法 --降权机制确保该算法合理  常用nginx等
// 算法原理：通过排序将当前权重最大的服务筛选出来返回，当命中服务后，被命中的服务当前权重减去总权重确保其变小不会被排序到头部，
// 每次选择前都加上每个服务自身权重使其能稳步增加，这样最后会形成总权重数量为循环且合理的服务加权轮询。
// 添加降权健康机制：改动部分，当前权重所加的该服务权重不再固定，通过即时计算所得（需要减去降权权重FailWeight）
func (this *LoadBalance) RoundRobinByWeight3() *HttpServer {
	//初始化(得到的结果为初始权重)，同时也是每次都要做的步骤，将当前权重加上该服务权重
	for _, s := range this.Servers {
		s.CWeight = s.CWeight + s.Weight - s.FailWeight //当前权重所加的该服务权重不再固定，通过即时计算所得（需要减去降权权重FailWeight）
	}
	sort.Sort(this.Servers) //排序 根据当前权重从大到小
	//输出一下当前server
	fmt.Println("this.Server:")
	this.Servers.CWeightToString()
	max := this.Servers[0] //排头即当前权重最大值 作为命中服务
	//命中后的权重需要减去总权重
	max.CWeight -= this.getSumWeight() //减去的总权重也要进行相应的减少，因为加的少了，减的也要少，这样才能确保动态平衡。//感觉要做的更多了
	//输出一下max.CWeight
	fmt.Println("max.CWeight:", max.CWeight)
	return max
}

// 初始化一个简易的httpCheck作为该负载均衡的健康检查
// 输出参数用于调试
func checkServers(lb *LoadBalance) {
	//创建一个定时器，每三秒执行一次  实际是每隔三秒往Channel里塞一个值 通过t.C判断 后修改成15
	t := time.NewTicker(time.Second * 15)
	check := NewSingleHttpChecker(lb)
	for {
		select {
		case <-t.C:
			//过期时间，后续补充
			check.Check(time.Second * 2)
			lb.Servers.StatusToString()
			fmt.Println(lb.DownServers)
			fmt.Println("------------------------------------------------")
		}
	}
}

// 初始化一个简易的httpCheck作为该负载均衡的健康检查，用于没配置健康检查参数时，自动使用默认值
// 输出参数用于调试
func normalCheckServers(lb *LoadBalance) {

}

// 判断该负载均衡器内的服务是否全部被down
func (this *LoadBalance) IsAllDown() bool {
	//感觉是可以优化的，比如说直接在LoadBalance里加一个字段表明为down机数量，和一个字段为服务器数量，当down机数和服务器数相等时便是全部宕机
	/*
		downCount:=0
		for _,s:=range this.Servers{
			if s.Status=="DOWN"{
				downCount++
			}
		}
		if downCount==len(this.Servers){
			return true
		}
		return false*/
	if this.DownServers == len(this.Servers) {
		return true
	}
	return false
}

// 即时计算总权重
// 更改逻辑
func (this *LoadBalance) getSumWeight() int {
	sum := this.WeightCount
	if sum == 0 {
		for _, server := range this.Servers {
			sum += server.Weight - server.FailWeight
			this.WeightCount += server.Weight //补充进去 后面就算直接在路由器里添加server也不怕，那边会直接在weightCount中加入值
		}
	} else {
		for _, server := range this.Servers {
			sum -= server.FailWeight
		}
	}

	return sum
}

// 根据该负载均衡器设定的负载均衡方式返回合适的服务路由 尝试过用模仿java时使用的策略模式使用一个Map存储，但是好像改造难度有点大因此选择使用简便方法。
func (this *LoadBalance) GetServer(ip string) *HttpServer {
	var hs *HttpServer
	//go的switch不需要break
	switch this.LoadBalanceWay {
	case POLLING:
		hs = this.RoundRobin()

	case SWPOLLING:
		hs = this.RoundRobinByWeight3()

	case IPHASH:
		hs = this.SelectByIPHash(ip)

	default:
		log.Fatal("please check your loadBalance configure properties:  loadBalanceWay")
	}
	return hs
}
